# Copyright 1998-2011 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

import errno
import logging
import sys

try:
	import cPickle as pickle
except ImportError:
	import pickle

from portage import os
from portage import _encodings
from portage import _os_merge
from portage import _unicode_decode
from portage import _unicode_encode
from portage.exception import PermissionDenied
from portage.localization import _
from portage.util import atomic_ofstream
from portage.util import writemsg_level
from portage.versions import cpv_getkey
from portage.locks import lockfile, unlockfile

if sys.hexversion >= 0x3000000:
	basestring = str

class PreservedLibsRegistry(object):
	""" This class handles the tracking of preserved library objects """
	def __init__(self, root, filename):
		""" 
			@param root: root used to check existence of paths in pruneNonExisting
		    @type root: String
			@param filename: absolute path for saving the preserved libs records
		    @type filename: String
		"""
		self._root = root
		self._filename = filename
		self._data = None
		self._lock = None

	def lock(self):
		"""Grab an exclusive lock on the preserved libs registry."""
		if self._lock is not None:
			raise AssertionError("already locked")
		self._lock = lockfile(self._filename)

	def unlock(self):
		"""Release our exclusive lock on the preserved libs registry."""
		if self._lock is None:
			raise AssertionError("not locked")
		unlockfile(self._lock)
		self._lock = None

	def load(self):
		""" Reload the registry data from file """
		self._data = None
		f = None
		try:
			f = open(_unicode_encode(self._filename,
					encoding=_encodings['fs'], errors='strict'), 'rb')
			if os.fstat(f.fileno()).st_size == 0:
				# ignore empty lock file
				pass
			else:
				self._data = pickle.load(f)
		except (AttributeError, EOFError, ValueError, pickle.UnpicklingError) as e:
			writemsg_level(_("!!! Error loading '%s': %s\n") % \
				(self._filename, e), level=logging.ERROR, noiselevel=-1)
		except EnvironmentError as e:
			if not hasattr(e, 'errno'):
				raise
			elif e.errno == errno.ENOENT:
				pass
			elif e.errno == PermissionDenied.errno:
				raise PermissionDenied(self._filename)
			else:
				raise
		finally:
			if f is not None:
				f.close()
		if self._data is None:
			self._data = {}
		self._data_orig = self._data.copy()
		self.pruneNonExisting()

	def store(self):
		"""
		Store the registry data to the file. The existing inode will be
		replaced atomically, so if that inode is currently being used
		for a lock then that lock will be rendered useless. Therefore,
		it is important not to call this method until the current lock
		is ready to be immediately released.
		"""
		if os.environ.get("SANDBOX_ON") == "1" or \
			self._data == self._data_orig:
			return
		try:
			f = atomic_ofstream(self._filename, 'wb')
			pickle.dump(self._data, f, protocol=2)
			f.close()
		except EnvironmentError as e:
			if e.errno != PermissionDenied.errno:
				writemsg_level("!!! %s %s\n" % (e, self._filename),
					level=logging.ERROR, noiselevel=-1)
		else:
			self._data_orig = self._data.copy()

	def _normalize_counter(self, counter):
		"""
		For simplicity, normalize as a unicode string
		and strip whitespace. This avoids the need for
		int conversion and a possible ValueError resulting
		from vardb corruption.
		"""
		if not isinstance(counter, basestring):
			counter = str(counter)
		return _unicode_decode(counter).strip()

	def register(self, cpv, slot, counter, paths):
		""" Register new objects in the registry. If there is a record with the
			same packagename (internally derived from cpv) and slot it is 
			overwritten with the new data.
			@param cpv: package instance that owns the objects
			@type cpv: CPV (as String)
			@param slot: the value of SLOT of the given package instance
			@type slot: String
			@param counter: vdb counter value for the package instance
			@type counter: String
			@param paths: absolute paths of objects that got preserved during an update
			@type paths: List
		"""
		cp = cpv_getkey(cpv)
		cps = cp+":"+slot
		counter = self._normalize_counter(counter)
		if len(paths) == 0 and cps in self._data \
				and self._data[cps][0] == cpv and \
				self._normalize_counter(self._data[cps][1]) == counter:
			del self._data[cps]
		elif len(paths) > 0:
			self._data[cps] = (cpv, counter, paths)

	def unregister(self, cpv, slot, counter):
		""" Remove a previous registration of preserved objects for the given package.
			@param cpv: package instance whose records should be removed
			@type cpv: CPV (as String)
			@param slot: the value of SLOT of the given package instance
			@type slot: String
		"""
		self.register(cpv, slot, counter, [])
	
	def pruneNonExisting(self):
		""" Remove all records for objects that no longer exist on the filesystem. """

		os = _os_merge

		for cps in list(self._data):
			cpv, counter, paths = self._data[cps]
			paths = [f for f in paths \
				if os.path.exists(os.path.join(self._root, f.lstrip(os.sep)))]
			if len(paths) > 0:
				self._data[cps] = (cpv, counter, paths)
			else:
				del self._data[cps]
	
	def hasEntries(self):
		""" Check if this registry contains any records. """
		if self._data is None:
			self.load()
		return len(self._data) > 0
	
	def getPreservedLibs(self):
		""" Return a mapping of packages->preserved objects.
			@returns mapping of package instances to preserved objects
			@rtype Dict cpv->list-of-paths
		"""
		if self._data is None:
			self.load()
		rValue = {}
		for cps in self._data:
			rValue[self._data[cps][0]] = self._data[cps][2]
		return rValue
